package app;

import exceptions.LexicoException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;

/**
 *
 * @author Julio
 */
public class Lexico {
    
    private FileInputStream in;
    
    private LinkedList<Token> list;
    
    private boolean entrada;

    private int nextChar;
    private int line;

    private static final char EOF = '\uFFFF';
    
    public Lexico(String file) throws FileNotFoundException{
        in = new FileInputStream(file);
        
        list = new LinkedList<Token>();

        entrada = true;
        
        nextChar = -1;
        line = 1;
    }
    
    public int getLine(){
        return this.line;
    }
    
    /**
     * Metodo responsavel por fazer a exclusao dos comentarios dentro de um
     * determinado programa.
     * 
     * @throws java.io.IOException
     * @throws exceptios.LexicoException
     */
    private void excludeComments() throws IOException, LexicoException{
        if(nextChar == '{') {
            do {
                nextChar = nextChar();
                
                if(nextChar == '\n') {
                    line++;
                }

                //se chegar ao fim do arquivo e nao identificar o fechamento
                //dos comentarios, lança excessao.
                if(nextChar == EOF) {
                    throw new LexicoException(LexicoException.OPENED_COMMENTS_NOT_CLOSED);
                }

            } while(nextChar != '}');

            
            nextChar = nextChar();

        }
    }
    
    /**
     * Metodo responsavel por retornar o proximo caractere do arquivo.
     * 
     * @return o proximo caractere do arquivo
     * @throws exceptios.LexicoException
     * @throws java.io.IOException
     */
    private char nextChar() throws IOException, LexicoException{        
        return (char)(nextChar = in.read());
    }
    
    /**
     * Metodo responsavel por retornar o proximo caractere que nao seja identação,
     * nova linha, espaço em branco e comentario.
     * 
     * @return retornar o proximo caractere
     * @throws java.io.IOException
     * @throws exceptios.LexicoException
     */
    private char nextNonBlankChar() throws IOException, LexicoException{

        excludeComments();

        while(nextChar == '\t' || nextChar == '\n' || nextChar == ' ' || nextChar == '\r' || entrada) {
            entrada = false;
            nextChar = nextChar();
            
            excludeComments();
            
            if(nextChar == '\n') {
                line++;
            }          
        } 

        return (char) nextChar;
    }

    /**
     * Verifica se o char corrente é letra.
     * 
     * @param c char corrente
     * @return true se for uma letra
     */
    private boolean isLetter(char c){
        return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
    }
    
    /**
     * Verifica se o char corrente é numero.
     * 
     * @param c char corrente
     * @return true se for um numero
     */
    private boolean isNumber(char c){
        return (c >= '0' && c <= '9');
    }
    
    /**
     * Metodo responsavel por verificar se determinada string é palavra reservada
     * ou identificador.
     *  
     * @param simbol string construida 
     * @return o codigo da string
     */
    private int codeOf(StringBuilder simbol){
        String token = simbol.toString();
        
        switch(token.charAt(0)){
            case 'a':
                if(token.equals("and")) return Code.AND;
                return Code.ID;
            case 'b':
                if(token.equals("begin")) return Code.BEGIN;
                if(token.equals("boolean")) return Code.BOOLEAN;
                return Code.ID;    
            case 'd':
                if(token.equals("do")) return Code.DO;
                return Code.ID;
            case 'e':
                if(token.equals("end")) return Code.END;
                if(token.equals("else")) return Code.ELSE;
                return Code.ID;
            case 'f':
                if(token.equals("false")) return Code.FALSE;
                return Code.ID;
            case 'i':
                if(token.equals("if")) return Code.IF;
                if(token.equals("integer")) return Code.INTEGER;
                return Code.ID;
            case 'n':
                if(token.equals("not")) return Code.NOT;
                return Code.ID;
            case 'o':
                if(token.equals("or")) return Code.OR;
                return Code.ID;    
            case 'p':
                if(token.equals("program")) return Code.PROGRAM;
                if(token.equals("procedure")) return Code.PROCEDURE;
                return Code.ID;
            case 'r':
                if(token.equals("real")) return Code.REAL;
                return Code.ID;   
            case 't':
                if(token.equals("then")) return Code.THEN;
                if(token.equals("true")) return Code.TRUE;
                return Code.ID;    
            case 'v':
                if(token.equals("var")) return Code.VAR;
                return Code.ID;
            case 'w':
                if(token.equals("while")) return Code.WHILE;
                return Code.ID;                    
            default:
                return Code.ID;
        }
    }
    
    /**
     * Metodo responsavel por adicionar todas as letras sequenciais a uma
     * string.
     * 
     * @param ch
     * @return o token setado com seu respectivo valor adquirido.
     * @throws java.io.IOException
     * @throws exceptios.LexicoException
     */
    private Token identifierToken(char ch) throws IOException, LexicoException{
        StringBuilder simbol = new StringBuilder();
        
        do{
            simbol.append(ch);
            ch = nextChar();
        }while(isLetter(ch) || isNumber(ch) || ch == '_');
        
        return new Token(codeOf(simbol), simbol.toString(), line);
    }
    
    /**
     * Metodo responsavel por adicionar todos os numeros sequenciais a uma
     * string.
     * 
     * @param ch
     * @return o token setado com seu respectivo valor adquirido.
     * @throws java.io.IOException
     * @throws exceptios.LexicoException
     */
    private Token numberToken(char ch) throws IOException, LexicoException{
        int type;
        StringBuilder simbol = new StringBuilder();
        
        //adiciona todos os numeros antes do ponto
        while(isNumber(ch)){
            simbol.append(ch);
            ch = nextChar();
        }
        
        //se tiver o ponto, adiciona o ponto
        if(ch == '.'){
            type = Code.REAIS;
            simbol.append(ch);
            ch = nextChar();
            
            //adiciona todos os numeros depois do ponto
            while(isNumber(ch)){
                simbol.append(ch);
                ch = nextChar();
            }
        }
        else{
            type = Code.INTEIRO;
        }
                
        return new Token(type, simbol.toString(), line);
    }
    
    /**
     * Metodo responsavel pegar o proximo token.
     * 
     * @return retorna o proximo token.
     */
    private Token nextToken() throws IOException, LexicoException{
        char character;
                
        character = nextNonBlankChar();

        switch(character){
            case '(': 
                nextChar = nextChar();
                return new Token(Code.ABRE_PARENTESES, "(", line);
            case ')': 
                nextChar = nextChar();
                return new Token(Code.FECHA_PARENTESES, ")", line);
            case '+': 
                nextChar = nextChar();
                return new Token(Code.ADICAO, "+", line);
            case '-': 
                nextChar = nextChar();
                return new Token(Code.SUBTRACAO, "-", line);
            case '/': 
                nextChar = nextChar();
                return new Token(Code.DIVISAO, "/", line);
            case '*': 
                nextChar = nextChar();
                return new Token(Code.MULTIPLICACAO, "*", line);
            case ';': 
                nextChar = nextChar();
                return new Token(Code.PONTO_E_VIRGULA, ";", line);
            case '.': 
                nextChar = nextChar();
                return new Token(Code.PONTO_FINAL, ".", line);
            case ':':
                if(nextChar() == '=') {
                    nextChar = nextChar();
                    return new Token(Code.ATRIBUICAO, ":=", line);
                }
                else {
                    return new Token(Code.DOIS_PONTOS, ":", line);
                }
            case '=':
                nextChar = nextChar();
                return new Token(Code.IGUAL, "=", line);
            case '>':
                if(nextChar() == '=') {
                    nextChar = nextChar();
                    return new Token(Code.MAIOR_IGUAL, ">=", line);
                }
                else {
                    nextChar = nextChar();
                    return new Token(Code.MAIOR_QUE, ">", line);
                }
            case '<':
                if(nextChar() == '=') {
                    nextChar = nextChar();
                    return new Token(Code.MENOR_IGUAL, "<=", line);
                }
                else if(nextChar == '>') {
                    nextChar = nextChar();
                    return new Token(Code.DIFERENTE, "<>", line);
                }
                else {
                    nextChar = nextChar();
                    return new Token(Code.MENOR_QUE, "<", line);
                }
            case ',':
                nextChar = nextChar();
                return new Token(Code.VIRGULA, ",", line);
        }
        
        //testar se eh numero
        if(isNumber(character)){
            return numberToken(character);
        }
        
        //testa se eh um identificador
        if(isLetter(character)){
            return identifierToken(character);
        }
        
        //se não for o fim do arquivo, e encontrar um caractere invalido
        if(character != EOF){
            throw new LexicoException(new Token(Code.ERRO, String.valueOf(character), line));
        }          

        return null;
    }
    
    /**
     * Metodo responsavel por pegar todos os tokens pertencentes a linguagem.
     * @return a lista de tokens
     */
    public LinkedList<Token> getList() throws IOException, LexicoException{
        Token t;
        
        while((t = nextToken()) != null){
            list.add(t);
        }
        
        return this.list;
    }
}
